Skip to content

Defer tail call ret ty equality to check_tail_calls #144915

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

compiler-errors
Copy link
Member

@compiler-errors compiler-errors commented Aug 4, 2025

Fixes #144892.

Currently the tail call signature check assumes that return types have been accounted for. However, this is not complete for several reasons.

Firstly, we were using subtyping instead of equality in the HIR typeck code:

self.demand_suptype(expr.span, ret_ty, call_expr_ty);

We could fix this, but it doesn't really do much for us anyways since HIR typeck doesn't care about regions.

That means, secondly, we'd need to fix the terminator type check in MIR typeck to account for variances, since tail call terminators need to relate their arguments invariantly to account for the "signature must be equal" rule. This seems annoying.

All of this seems like a lot of work, and we already are manually checking argument equality. Let's just extend the check_tail_calls to account for mismatches in return types anyways.

r? @WaffleLapkin

@rustbot
Copy link
Collaborator

rustbot commented Aug 4, 2025

WaffleLapkin is not on the review rotation at the moment.
They may take a while to respond.

@rustbot rustbot added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. labels Aug 4, 2025
@theemathas

This comment was marked as off-topic.

@compiler-errors

This comment was marked as off-topic.

@WaffleLapkin WaffleLapkin added the F-explicit_tail_calls `#![feature(explicit_tail_calls)]` label Aug 5, 2025
@@ -1931,7 +1931,7 @@ impl<'a, 'tcx> TypeChecker<'a, 'tcx> {
self.tcx(),
self.infcx.typing_env(self.infcx.param_env),
) {
span_mirbug!(self, term, "call to converging function {:?} w/o dest", sig);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isn't the point here that the function does not diverge (as its return type is not uninhabited), but we don't have a dest?

as in, the comment was correct

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hm... i think i see, but in that case i would call it non-diverging

@@ -118,21 +118,7 @@ impl<'tcx> TailCallCkVisitor<'_, 'tcx> {
}

if caller_sig.inputs_and_output != callee_sig.inputs_and_output {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please also add a test that we anonymize bound vars: for<'a> fn(&'a ()) and for<'b> fn(&'b ())

@lcnr
Copy link
Contributor

lcnr commented Aug 5, 2025

r? lcnr

r=me after nits

@rustbot rustbot assigned lcnr and unassigned WaffleLapkin Aug 5, 2025
@WaffleLapkin
Copy link
Member

That means, secondly, we'd need to fix the terminator type check in MIR typeck to account for variances, since tail call terminators need to relate their arguments invariantly to account for the "signature must be equal" rule. This seems annoying.

That doesn't seem to track for me 🤔

The only reason we care about signature equality is that we need ABIs to match exactly, but ABIs are completely unaffected by lifetimes, so for the purpose of lifetimes we can do the same as normal call+ret.

Comment on lines -125 to -132
// FIXME(explicit_tail_calls): this currently fails for cases where opaques are used.
// e.g.
// ```
// fn a() -> impl Sized { become b() } // ICE
// fn b() -> u8 { 0 }
// ```
// we should think what is the expected behavior here.
// (we should probably just accept this by revealing opaques?)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you keep this fixme? (it doesn't ICE anymore, but doesn't work either...)

Comment on lines +8 to +9
= note: caller signature: `fn() -> for<'a> fn(&'a i32)`
= note: callee signature: `fn() -> fn(&i32)`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we not printing 'static? This looks so confusing 😵‍💫

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because we erase regions. We could print the unerased signature, but this has nothing to do with static, and it is a preexisting issue with args.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

preexisting example:

#![feature(explicit_tail_calls)]
#![allow(incomplete_features)]

fn foo<'a>(_: fn(&'a ())) {
    become bar(dummy);
}

fn bar(_: fn(&())) {}

fn dummy(_: &()) {}

fn main() {}
error: mismatched signatures
 --> src/main.rs:5:5
  |
5 |     become bar(dummy);
  |     ^^^^^^^^^^^^^^^^^
  |
  = note: `become` requires caller and callee to have matching signatures
  = note: caller signature: `fn(fn(&()))`
  = note: callee signature: `fn(for<'a> fn(&'a ()))`

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've opened #144957 so that we don't forget to return to this at some point

@compiler-errors
Copy link
Member Author

compiler-errors commented Aug 5, 2025

That doesn't seem to track for me 🤔

The only reason we care about signature equality is that we need ABIs to match exactly, but ABIs are completely unaffected by lifetimes, so for the purpose of lifetimes we can do the same as normal call+ret.

They are affected by higher-ranked lifetimes, though, and higher-ranked subtyping is different than higher-ranked equality. So what I'm saying is that checking this the way that normal return types are checked is (theoretically) insufficient since they don't enforce return type equality, but just a subtyping relationship between the thing being returned by the callee and the thing being returned by the caller.


That being said, do we even have a case where a non-invariant arg causes a struct or something to have a different layout so that a subtyping operation would change its layout?

@theemathas
Copy link
Contributor

A type and its subtype always has the same layout and ABI, since fn() -> Subtype can be coerced to a fn() -> Supertype without changing the machine code.

Copy link
Member

@WaffleLapkin WaffleLapkin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this looks like a reasonable step. r=us with nits

if caller_sig.output() != callee_sig.output() {
span_bug!(expr.span, "hir typeck should have checked the return type already");
}
self.report_arguments_mismatch(expr.span, caller_sig, callee_sig);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: the function should probably be renamed to report_signature_mismatch

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
F-explicit_tail_calls `#![feature(explicit_tail_calls)]` S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Explicit tail call returning a HRTB causes ICE: "hir typeck should have checked the return type already"
5 participants